package com.jappydb;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;

import freemarker.template.SimpleCollection;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;

/**
 * JObject is an object node. It contains various key, value players. The value
 * can be a String or an Array. Direct read-write API is supported and so are
 * nested keys.
 * 
 * @author harsh
 */
public class JObject implements TemplateHashModelEx {

	protected String mKey;
	
	/**
	 * Our State variables.
	 */
	protected final JContext mCtxt;

	/**
	 * We store the objects in a concurrent map.
	 */
	protected ConcurrentHashMap<String, Object> mO;

	/**
	 * Constructor.
	 * 
	 * @param key
	 * @param parent
	 * @param ctxt
	 */
	public JObject(JContext ctxt) {
		this.mKey = "";
		this.mCtxt = ctxt;
		this.mO = new ConcurrentHashMap<String, Object>();
	}
	
	/**
	 * @param o
	 * @throws JSONException 
	 */
	public JObject(JContext ctxt, JSONObject o) throws JSONException {
		this(ctxt);
		
		Iterator<String> keys = o.keys();
		while (keys.hasNext()) {
			String ckey = keys.next();
			put(ckey, JUtils.objectToJ(ctxt, o.get(ckey)));
		}
	}

	/**
	 * The delete should first find the parent object containing the key and
	 * then remove it from its hashmap. Note we dont go and delete the disk
	 * stuff. Which would make delete expensive.
	 * 
	 * @param key
	 */
	public void delete(String key) {
		String pkey = JUtils.getParentKey(key);
		String skey = JUtils.getShortKey(key);

		// This is the object we will act upon.
		JObject ao = this;
		if (pkey != null) {
			if (!hasJObject(pkey)) {
				throw new IllegalArgumentException("Sorry cant delete.");
			}
			ao = getJObject(pkey);
		}
		
		ao.deleteDirect(skey);
	}
	
	protected void deleteDirect(String key) {
		mO.remove(key);
	}

	/**
	 * Return the string associated with the given key.
	 * 
	 * @param key
	 * @param def
	 * @return
	 */
	public String getString(String key, String def) {
		key = key.toLowerCase();
		Object o = getRaw(key);
		if (o == null)
			return def;
		if (!(o instanceof String))
			throw new IllegalArgumentException("key is not a string");
		return (String) o;
	}

	/**
	 * JArray pointing to the key.
	 * 
	 * @param key
	 * @return
	 */
	public JArray getJArray(String key) {
		Object o = getRaw(key);
		if (o == null)
			return null;
		if (o instanceof JArray)
			return (JArray) o;
		throw new JException(key + " is not a JArray");
	}

	/**
	 * Return JObject.
	 * 
	 * @param key
	 * @return
	 */
	public JObject getJObject(String key) {
		Object o = getRaw(key);
		if (o == null || o.equals(null))
			return null;
		if (o instanceof JObject)
			return (JObject) o;
		throw new JException(key + " is not a JObject");
	}

	/**
	 * Existency checks.
	 * 
	 * @param key
	 * @return
	 */
	public boolean has(String key) {
		return hasString(key) || hasJObject(key);
	}

	/**
	 * @param key
	 * @return
	 */
	public boolean hasJArray(String key) {
		Object o = getRaw(key);
		return (o != null && (o instanceof JArray));
	}

	/**
	 * @param key
	 * @return
	 */
	public boolean hasJObject(String key) {
		Object o = getRaw(key);
		return (o != null && (o instanceof JObject));
	}

	/**
	 * @param key
	 * @return
	 */
	public boolean hasString(String key) {
		Object o = getRaw(key);
		return (o != null && (o instanceof String));
	}

	/**
	 * Put key,value pair. Value is string. and Here key can be a.b.c
	 * 
	 * @param key
	 * @param value
	 */
	public void put(String key, String value) {
		LOG.trace(String.format("put key=%s value=%s", key, value));
		key = key.toLowerCase();
		JObject o = ensureJObject(key);
		String skey = JUtils.getShortKey(key);
		o.mO.put(skey, value);
	}

	/**
	 * Whether we are empty.
	 */
	public boolean isEmpty() throws TemplateModelException {
		return mO.isEmpty();
	}

	/**
	 * List of all the keys.
	 * 
	 * @return
	 */
	public List<String> keyList() {
		return new ArrayList<String>(mO.keySet());
	}

	public TemplateCollectionModel keys() {
		return new SimpleCollection(mO.keySet());
	}

	/**
	 * Insert a JObject.
	 * 
	 * @param key
	 * @param value
	 */
	public void put(String key, JObject value) {
		key = key.toLowerCase();
		JObject kid = ensureJObject(key);
		value.mKey = JUtils.getShortKey(key);
		kid.mO.put(value.mKey, value);
	}

	/**
	 * We are about to add the JArray thing.
	 * 
	 * @param key
	 * @return
	 */
	public void put(String key, JArray value) {
		key = key.toLowerCase();
		JObject kid = ensureJObject(key);
		value.mKey = JUtils.getShortKey(key);
		kid.mO.put(value.mKey, value);
	}
	
	public void put(String key, Object value) {
		if (value instanceof JArray) {
			put(key, (JArray) value);
		} else if (value instanceof JObject) {
			put(key, (JObject) value);
		} else if (value instanceof String) {
			put(key, (String) value);
		} else {
			throw new IllegalArgumentException();
		}
	}

	public int size() throws TemplateModelException {
		return mO.size();
	}

	public Collection<String> stringKeys() {
		return mO.keySet();
	}

	public TemplateCollectionModel values() throws TemplateModelException {
		throw new UnsupportedOperationException();
	}

	/**
	 * Supporting Freemarker.
	 */
	public TemplateModel get(String key) throws TemplateModelException {
		return JUtils.wrap(getRaw(key));
	}

	/**
	 * @return
	 * @throws JSONException
	 */
	public JSONObject toJSONObject() throws JSONException {
		JSONObject ret = new JSONObject();
		Enumeration<String> keys = mO.keys();
		while (keys.hasMoreElements()) {
			String key = keys.nextElement();
			Object value = mO.get(key);
			
			if (value instanceof JArray) {
				ret.put(key, ((JArray) value).toJSONArray());
			} else if (value instanceof JObject) {
				ret.put(key, ((JObject) value).toJSONObject());
			} else {
				ret.put(key, value.toString());
			}
		}
		return ret;
	}

	public JArray createOrGet(String key) {
		if (hasJArray(key)) {
			return getJArray(key);
		}
		JArray newArray = new JArray(mCtxt);
		put(key, newArray);
		return newArray;
	}
	
	/**
	 * Create a chain of JObject to ensure that the given key is represented.
	 * 
	 * @param key
	 * @return
	 */
	private JObject ensureJObject(String key) {
		String sp[] = key.split("\\.");
		JObject o = this;
		for (int k = 0; k < sp.length - 1; ++k) {
			Object value = o.mO.get(sp[k]);
			if (value == null || value.equals(null)) {
				value = new JObject(mCtxt);
				o.mO.put(sp[k], value);
			}
			o = (JObject) value;
		}
		return o;
	}

	/**
	 * Return the JObject associated with the given key or null. This is the Raw
	 * stuff stored in the memory cache.
	 * 
	 * @param key
	 * @return
	 */
	private Object getRaw(String key) {
		if (key == null) {
			throw new IllegalArgumentException("key_is_null");
		}
		if (key.length() == 0) {
			return this;
		}
		key = key.toLowerCase();
		if (key.equals("key")) {
			return mKey;
		}
		String sp[] = key.split("\\.");
		JObject o = this;
		for (int s = 0; s < sp.length - 1; ++s) {
			Object v = o.mO.get(sp[s]);
			if (v == null || !(v instanceof JObject))
				return null;
			o = (JObject) o.mO.get(sp[s]);
		}
		return o.mO.get(sp[sp.length - 1]);
	}

	/**
	 * Logger
	 */
	private static Logger LOG = Logger.getLogger(JObject.class);

	public JContext getContext() {
		return mCtxt;
	}

	
}
